package fallowme.comm;

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

import fallowme.comm.ClientSideMessage;
import fallowme.comm.ServerSideMessage;
import fallowme.comm.ClientSideMessage.Type;



/**
 * Class that represents the communication  of the Birdsong server. This is the only
 * way that the Birdsong server can communicate with their clients.

 * @author Bruno Parra, Leo Dias, Miguel Lemos, Ruben Gomes
 * @version 1.0
 */
public class BirdSongServerComm  implements ServerComm{

	public static final int SERVER_PORT = 8080;
	private LinkedBlockingQueue<BirdSongServerSideMessage>  incomingMessages;
	private Map<String, ObjectOutputStream> outChannels = new HashMap<String, ObjectOutputStream>();
	private AcceptIncomingConnections acceptIncomingConnections;
	private boolean stopAcceptingConnections = false;

	/**
	 * Thread used to accept incoming client connections.
	 * @author Leo
	 */
	private class AcceptIncomingConnections extends Thread {
		private ServerSocket serverSocket;
		@Override
		public void run() {
			try {
				serverSocket = new ServerSocket(SERVER_PORT);
			} catch (IOException e) {
				System.out.println("Failed to create server socket.");
				e.printStackTrace();
			}
			try {
				while (!stopAcceptingConnections) {	
					assert !serverSocket.isClosed();
					Socket socket  = serverSocket.accept();
					//new thread created to deal with the new client, has a reference to the output channels
					new DealWithClient(outChannels, socket, incomingMessages).start();
				}
			} catch (IOException e) {
				System.out.println("Failed to accept the connection.");
				e.printStackTrace();
			} finally {
				try {
					System.out.println("im out");
					serverSocket.close();
				} catch (IOException e) {
					System.out.println("Failed to close server socket");
					e.printStackTrace();
				}
			}
		}
	}

	/**
	 * Creates a new BirdSong server initializing the list incomingMessages{@link BlockingQueue}
	 * and the thread to acceptIncommingConnections.
	 */
	public BirdSongServerComm (){
		this.incomingMessages = new LinkedBlockingQueue<BirdSongServerSideMessage>();
		this.acceptIncomingConnections = new AcceptIncomingConnections();
	}	

	/**
	 * This method starts the acceptIncomingConnections thread, returning immediately
	 * after doing so.
	 */
	@Override
	public void start() {
		acceptIncomingConnections.start();
	}

	/**
	 * Gets the oldest message in the queue, If no message 
	 * has been received, the method blocks until a message has 
	 * been received.
	 * @return the oldest message in the queue.
	 */
	@Override
	public ServerSideMessage getNextMessage() {
		ServerSideMessage newMessage = null;
		try {
			newMessage = incomingMessages.take();
			if(newMessage.getType().equals(ServerSideMessage.Type.CLIENT_CONNECTED)||
					newMessage.getType().equals(ServerSideMessage.Type.CLIENT_DISCONNECTED)){
				assert newMessage.getPayload()== null;
			}
			else assert newMessage.getPayload() != null;
		} catch (InterruptedException e) {
			System.out.println("Failed to retrieve next message.");
		}
		return newMessage;
	}

	/**
	 * Checks if the queue is not empty.
	 * 
	 * @return true if there is a message in the queue, false if not. 
	 */
	@Override
	public boolean hasNextMessage() {
		return incomingMessages.peek()!=null;
	}

	/**
	 * Sends a {@link ClientSideMessage} of type BIRDSONG_MESSAGE.
	 * 
	 * @param fromNicename , the nickname of the sender.
	 * @param toNickname, the nickname of the receiver.
	 * @param birdsongMessage, the body of the message.
	 */
	@Override
	public void sendBirdsongMessage(String fromNicename, String toNickname,
			String birdsongMessage) {
		try {
			//browses toNickname channel in outChannels and writes a new message from fromNicename in it.
			outChannels.get(toNickname).writeObject(new BirdSongClientSideMessage(Type.BIRDSONG_MESSAGE, birdsongMessage, fromNicename));
		} catch (IOException e) {
			System.out.println("Failed to send birdsong message.");
		}
	}

	/**
	 * Sends a {@link ClientSideMessage} message of type FOLLOWER.
	 * 
	 * @param newFollower , the nickname of the follower.
	 * @param toNickname, the nickname of the client being followed.
	 */
	@Override
	public void sendNewFollower(String toNickname, String newFollower) {
		try {
			//browses toNickname channel in outChannels 
			//and writes the name of the new follower in it.
			outChannels.get(toNickname).writeObject(
					new BirdSongClientSideMessage(Type.FOLLOWER,
							newFollower, newFollower));
		} catch (IOException e) {
			System.out.println("Failed to send new follower message.");
		}

	}

	/**
	 * Sends a {@link ClientSideMessage} message of type UNFOLLOWER.
	 * 
	 * @param unfollower , the nickname of the  client that will stop following.
	 * @param toNickname, the nickname of the client not being followed.
	 */
	@Override
	public void sendNewUnfollow(String toNickname, String unfollower) {
		try {
			//browses toNickname channel in outChannels
			//and writes the name of the unfollower in it.
			outChannels.get(toNickname).writeObject(
					new BirdSongClientSideMessage(Type.UNFOLLOWER,
							unfollower, unfollower));
		} catch (IOException e) {
			System.out.println("Failed to send unfollow message.");
		}
	}

	/**
	 * Sends a {@link ClientSideMessage} message of type ERROR.
	 * 
	 * @param toNickname, the nickname of the client receiving the error message.
	 * @param error, an error description.
	 */
	@Override
	public void sendError(String toNickname, String error) {
		try {
			//browses toNickname channel in outChannels
			//and writes the description of the error in it.
			outChannels.get(toNickname).writeObject(
					new BirdSongClientSideMessage(Type.ERROR,
							error, BirdSongClientSideMessage.SERVER));
		} catch (IOException e) {
			System.out.println("Failed to send error message.");
		}
	}

	/**
	 * Sends a {@link ClientSideMessage} message of type CONNECTED.
	 * 
	 * @param toNickname, the nickname of the client receiving the message.
	 * @param connectedNickname, the nickname's of the newly connected client.
	 */
	@Override
	public void sendClientConnected(String toNickname, String connectedNickname){
		try {
			//browses toNickname channel in outChannels
			//and writes the newly connected user in it.
			outChannels.get(toNickname).writeObject(
					new BirdSongClientSideMessage(Type.CONNECTED,
							connectedNickname,connectedNickname));
		} catch (IOException e) {
			System.out.println("Failed to send client connected message.");
		}
	}

	/**
	 * Sends a {@link ClientSideMessage} message of type DISCONNECTED.
	 * 
	 * @param toNickname, the nickname of the client receiving the message.
	 * @param connectedNickname, the nickname's of the newly disconnected client.
	 */
	@Override
	public void sendClientDisconnected(String toNickname,
			String disconnectedNickname) {
		try {
			//browses toNickname channel in outChannels
			//and writes the disconnected user in it.
			outChannels.get(toNickname).writeObject(
					new BirdSongClientSideMessage(Type.DISCONNECTED,
							disconnectedNickname, disconnectedNickname));
		} catch (IOException e) {
			System.out.println("Failed to send client disconnected message.");
		}

	}

	/**
	 * Checks if a client is connected.
	 * 
	 * @param nickname, the clients nickname.
	 * @return true if a client with such nickname exists false if not.
	 */
	@Override
	public boolean clientIsConnected(String nickname) {
		return outChannels.containsKey(nickname);
	}

	/**
	 * Disconnects a client, removing him from the list of outgoing channels.
	 *
	 * @param nickname, the client to disconnect.
	 */
	@Override
	public void disconnectClient(String nickname) {
		assert outChannels.containsKey(nickname);
		outChannels.remove(nickname);

	}

	/**
	 * Used to return a reference to the thread accepting connections.
	 * 
	 * @return acceptIncomingConnections thread.
	 */
	public Thread getAcceptingConnectionsThread (){
		return acceptIncomingConnections;
	}

	/**
	 * Stops the thread accepting connections.
	 */
	public void stopAcceptingConnections(){
		stopAcceptingConnections= true;
	}
}